/*
 * Copyright 2008-2015 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.github.geequery.springdata.repository.query;

import java.util.Collection;
import java.util.Collections;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.NoResultException;

import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.query.ParameterAccessor;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.ParametersParameterAccessor;
import org.springframework.util.Assert;

public abstract class GqQueryExecution {

    private static final ConversionService CONVERSION_SERVICE;

    static {
        ConfigurableConversionService conversionService = new DefaultConversionService();
        conversionService.removeConvertible(Collection.class, Object.class);
        // conversionService.addConverter(JpaResultConverters.BlobToByteArrayConverter.INSTANCE);
        CONVERSION_SERVICE = conversionService;
    }

    /**
     * Executes the given {@link AbstractStringBasedJpaQuery} with the given
     * {@link ParameterBinder}.
     * 
     * @param query
     *            must not be {@literal null}.
     * @param binder
     *            must not be {@literal null}.
     * @return
     */
    public Object execute(AbstractGqQuery query, Object[] values) {
        Assert.notNull(query, "Argument abstractGqQuery is required; it must not be null");
        Assert.notNull(values, "Argument values is required; it must not be null");

        Object result;

        try {
            result = doExecute(query, values);
        } catch (NoResultException e) {
            return null;
        }

        if (result == null) {
            return null;
        }
        GqQueryMethod queryMethod = query.getQueryMethod();
        Class<?> requiredType = queryMethod.getReturnType();
        if (void.class.equals(requiredType) || requiredType.isAssignableFrom(result.getClass())) {
            return result;
        }
        return CONVERSION_SERVICE.canConvert(result.getClass(), requiredType) ? CONVERSION_SERVICE.convert(result, requiredType) : result;
    }

    /**
     * Method to implement {@link AbstractStringBasedJpaQuery} executions by
     * single enum values.
     * 
     * @param query
     * @param binder
     * @return
     */
    protected abstract Object doExecute(AbstractGqQuery query, Object[] values);

    /**
     * Executes the {@link AbstractStringBasedJpaQuery} to return a
     * {@link org.springframework.data.domain.Page} of entities.
     */
    static class PagedExecution extends GqQueryExecution {
        private final Parameters<?, ?> parameters;

        public PagedExecution(Parameters<?, ?> parameters) {
            this.parameters = parameters;
        }

        @Override
        protected Object doExecute(AbstractGqQuery repositoryQuery, Object[] values) {
            long total = repositoryQuery.getResultCount(values);
            ParameterAccessor accessor = new ParametersParameterAccessor(parameters, values);
            Pageable pageable = accessor.getPageable();
            if (pageable.getOffset() >= total) {
                return new PageImpl<Object>(Collections.emptyList(), pageable, total);
            }
            List<?> content = repositoryQuery.getResultList(values, pageable);
            return new PageImpl<>(content, pageable, total);
        }
    }

    static class StreamExecution extends GqQueryExecution {
        private CollectionExecution ce;
        protected StreamExecution(CollectionExecution ce) {
            this.ce = ce;
        }

        protected Object doExecute(AbstractGqQuery query, Object[] values) {
            @SuppressWarnings("rawtypes")
            Collection coll=(Collection) ce.doExecute(query, values);
            //FIXME 由于底层对Stream还没有原生支持，使用这种方式仅仅在形势上满足的Stream的接口，但是没能体现流式非阻塞接口的优点。
            return coll.stream();
        }
    }

    static class CollectionExecution extends GqQueryExecution {
        @Override
        protected Object doExecute(AbstractGqQuery repositoryQuery, Object[] values) {
            return repositoryQuery.getResultList(values, null);
        }
    }

    /**
     * Executes a {@link AbstractStringBasedJpaQuery} to return a single entity.
     */
    static class SingleEntityExecution extends GqQueryExecution {
        @Override
        protected Object doExecute(AbstractGqQuery query, Object[] values) {
            return query.getSingleResult(values);
        }
    }

    /**
     * Executes a modifying query such as an update, insert or delete.
     */
    static class ModifyingExecution extends GqQueryExecution {
        private final EntityManagerFactory em;

        /**
         * Creates an execution that automatically clears the given
         * {@link EntityManager} after execution if the given
         * {@link EntityManager} is not {@literal null}.
         * 
         * @param em
         */
        public ModifyingExecution(GqQueryMethod method, EntityManagerFactory em) {
            Class<?> returnType = method.getReturnType();

            boolean isVoid = void.class.equals(returnType) || Void.class.equals(returnType);
            boolean isInt = int.class.equals(returnType) || Integer.class.equals(returnType);
            Assert.isTrue(isInt || isVoid, "Modifying queries can only use void or int/Integer as return type!");
            this.em = em;
        }

        @Override
        protected Object doExecute(AbstractGqQuery query, Object[] values) {
            int result = query.executeUpdate(values);
            return result;
        }
    }

    /**
     * {@link Execution} removing entities matching the query.
     * 
     * @since 1.6
     */
    static class DeleteExecution extends GqQueryExecution {
        @Override
        protected Object doExecute(AbstractGqQuery jpaQuery, Object[] values) {
            return jpaQuery.executeDelete(values);
            // List<?> resultList = jpaQuery.getResultList(values, null);
            // return jpaQuery.getQueryMethod().isCollectionQuery() ? resultList
            // : resultList.size();
        }
    }

    static class CountExecution extends GqQueryExecution {
        @Override
        protected Object doExecute(AbstractGqQuery jpaQuery, Object[] values) {
            return jpaQuery.getResultCount(values);
        }
    }
}
